﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Globalization;

using System.Diagnostics;
using System.Reflection;

using NHunspell;

namespace System.Windows.KExtensions.Adorners {

    /// <summary>
    ///  A control to view or edit styled text
    /// </summary>
    internal class SpellerAdorner : Adorner {

        private TextBox TextBox { get; set; }

        private System.Timers.Timer checkTimer;
        private const int SPELLCHECK_TIME = 1000;
        
        private string LastCheckedText { get; set; }
        private string LastText { get; set; }
        private string LanguageCode { get; set; }

        private List<PositionedWord> UnderlineMarks { get; set; }
        private List<string> WrongWords { get; set; }
        private List<string> CorrectWords { get; set; }
        private bool hasNewWords = false;

        private PositionedWord CurrentCorrectionPair { get; set; }
        private System.Windows.Controls.Primitives.Popup SuggestionsMenu { get; set; }
        private ListBox suggestionListBox;

        private int carretBeforOpen = 0;

        private bool dictionaryFailedToOpen = false;
        
        private bool mScrollingEventEnabled;

        public SpellerAdorner(UIElement adornedElement) : base(adornedElement) {

            TextBox = adornedElement as TextBox;

            if (TextBox == null)
                throw new Exception("SpellerAdorner can only be used at textbox controls.");

         
            UnderlineMarks = new List<PositionedWord>();
            CorrectWords = new List<string>();
            WrongWords = new List<string>();

            LastText = TextBox.Text;

            LanguageCode = SpellCheck.GetLanguage(TextBox);


        }

        protected override void OnInitialized(EventArgs e) {
            base.OnInitialized(e);
            
            TextBox.TextChanged += new TextChangedEventHandler(TextBox_TextChanged);

            TextBox.PreviewKeyDown += new KeyEventHandler(TextBox_PreviewKeyDown);
            
            TextBox.LostKeyboardFocus += new KeyboardFocusChangedEventHandler(TextBox_LostKeyboardFocus);
            
            TextBox.PreviewMouseRightButtonUp += new MouseButtonEventHandler(TextBox_MouseLeftButtonUp);

            TextBox.Unloaded += new RoutedEventHandler(TextBox_Unloaded);
        }

        void TextBox_Unloaded(object sender, RoutedEventArgs e) {
            TextBox.TextChanged -= new TextChangedEventHandler(TextBox_TextChanged);
            TextBox.PreviewKeyDown -= new KeyEventHandler(TextBox_PreviewKeyDown);
            TextBox.LostKeyboardFocus -= new KeyboardFocusChangedEventHandler(TextBox_LostKeyboardFocus);
            TextBox.PreviewMouseRightButtonUp -= new MouseButtonEventHandler(TextBox_MouseLeftButtonUp);
            TextBox.Unloaded -= new RoutedEventHandler(TextBox_Unloaded);
        }

        

        void TextBox_PreviewKeyDown(object sender, KeyEventArgs e) {
            if (checkTimer == null)
                StartCheck();
        }


        private void AdjustCaretIndex() {
            if (carretBeforOpen != TextBox.CaretIndex) {
                TextBox.CaretIndex = carretBeforOpen;
                carretBeforOpen = 0;
            }
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e) {
            LastText = TextBox.Text;
            MarkUnderlines();
        }

        private void StartCheck() {

            LastText = TextBox.Text;
            LastCheckedText = String.Empty;

            // sets the repaint timer            
            if (checkTimer == null) {
                checkTimer = new System.Timers.Timer(SPELLCHECK_TIME);
                checkTimer.Elapsed += new Timers.ElapsedEventHandler(checkTimer_Elapsed);
            }
            checkTimer.Start();
            
            //System.Console.Out.WriteLine(this.TextBox.GetHashCode() +  " ardoner " + this.GetHashCode().ToString() +  " starting time " + checkTimer.GetHashCode().ToString());

           
        }

        private void checkTimer_Elapsed(object sender, Timers.ElapsedEventArgs e) {
            CheckSpell();
        }

        private void TextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) {

            // if still have a suggstion menu opened, dont free resorcees yet
            if (SuggestionsMenu!=null && SuggestionsMenu.IsOpen)
                return;

            SuggestionsMenu = null;
           
            // release the checker timer
            if (checkTimer != null) {
                checkTimer.Stop();
                checkTimer.Dispose();
                checkTimer = null;
                //System.Console.Out.WriteLine("dispose time:" + checkTimer.GetHashCode().ToString());
            }

            

            // clear lists
            LastText = String.Empty;
            LastCheckedText = String.Empty;

            UnderlineMarks.Clear();
            this.InvalidateVisual();

            CorrectWords.Clear();
            WrongWords.Clear();

            MarkUnderlines();

        }

        private void AddMark(PositionedWord p) {
            PositionedWord mark = UnderlineMarks.SingleOrDefault(m => m.AtSamePosition(p));
            if (mark!=null)
                mark.Length = p.Length;
            else
                UnderlineMarks.Add(p);
        }

        private void CheckSpell() {

            if (SpellCheck.DisableAll || dictionaryFailedToOpen)
                return;

            // if needed repaint and was not painted since last time, paint it now
            if (hasNewWords)
                MarkUnderlines();

            hasNewWords = false;

            // if text has not changes since last check, get out
            if (LastCheckedText == LastText)
                return;

            LastCheckedText = LastText;

            List<PositionedWord> newWords = PositionedWord.FindAllPositionedWords(LastText);
            newWords = newWords.Where(w => !WrongWords.Contains(w.Word) && !CorrectWords.Contains(w.Word)).ToList();

            // if no new words, get out
            if (newWords.Count == 0)
                return;

            // check all new words
            using (Hunspell h = SpellCheck.GetHunspell(LanguageCode)) {
                if (h != null) { // hunspell my fail to initialize
                    foreach (PositionedWord word in newWords) {
                        bool correct = h.Spell(word.Word);
                        if (correct)
                            CorrectWords.Add(word.Word);
                        else
                            WrongWords.Add(word.Word);
                    }
                } else
                    dictionaryFailedToOpen = true;
            }

            // if words changed, then need repaint
            hasNewWords = true;
        }

        private void MarkUnderlines() {

            if (SpellCheck.DisableAll || dictionaryFailedToOpen)
                return;

            PositionedWord[] oldMarks = new PositionedWord[UnderlineMarks.Count];
            UnderlineMarks.CopyTo(oldMarks, 0);
            UnderlineMarks.Clear();
            List<PositionedWord> pairs = PositionedWord.FindAllPositionedWords(LastText);

            // mark all wrong words
            foreach (PositionedWord p in pairs.Where(p => WrongWords.Contains(p.Word)))
                AddMark(p);

            // detect if marks has changed
            bool sameMarks = oldMarks.All(o => UnderlineMarks.Any(m => m == o)) && UnderlineMarks.All(m => oldMarks.Any(o => o == m));

            // if changed, repaint it
            if (!sameMarks)
                Application.Current.Dispatcher.BeginInvoke((Action)delegate() { this.InvalidateVisual(); }, System.Windows.Threading.DispatcherPriority.Background);

            hasNewWords = false;

        }

        #region --- Sugestions

        private void CreateSuggestionMenu() {
            SuggestionsMenu = new System.Windows.Controls.Primitives.Popup() { IsOpen = false, Focusable =  false };
            FocusManager.SetIsFocusScope(SuggestionsMenu, false);
            SuggestionsMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Mouse;

            suggestionListBox = new ListBox() { BorderThickness = new Thickness(0), Background = Brushes.Transparent, Focusable = false };
            FocusManager.SetIsFocusScope(suggestionListBox, false);
            suggestionListBox.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);

            Style itemStyle = new Style() { TargetType = typeof(ListBoxItem) };
            itemStyle.Setters.Add(new Setter() { Property = ListBoxItem.FontSizeProperty, Value = 15.0 });
            itemStyle.Setters.Add(new Setter() { Property = ListBoxItem.BackgroundProperty, Value = Brushes.White });
            itemStyle.Setters.Add(new Setter() { Property = ListBoxItem.ForegroundProperty, Value = Brushes.Black });
            itemStyle.Setters.Add(new Setter() { Property = ListBoxItem.FontWeightProperty, Value = FontWeights.SemiBold });
            suggestionListBox.ItemContainerStyle = itemStyle;

            Border border = new Border() { BorderBrush = Brushes.Black, BorderThickness = new Thickness(2), Background = Brushes.White };
            SuggestionsMenu.Child = border;

            StackPanel stack = new StackPanel() { Orientation = System.Windows.Controls.Orientation.Vertical };
            border.Child = stack;

            stack.Children.Add(suggestionListBox);
            Button closeBtn = new Button() { Content = "none", Background = Brushes.White, BorderThickness = new Thickness(0, 0, 0, 0), Foreground = Brushes.Black, FontSize = 15.0 };
            closeBtn.Click += new RoutedEventHandler(closeBtn_Click);
            stack.Children.Add(closeBtn);
        }

        private void closeBtn_Click(object sender, RoutedEventArgs e) {
            if(SuggestionsMenu!=null)
                SuggestionsMenu.IsOpen = false;
        }

        private void list_SelectionChanged(object sender, SelectionChangedEventArgs e) {

            if (e.AddedItems.Count == 0)
                return;

            carretBeforOpen = TextBox.CaretIndex;

            string correctWord = e.AddedItems[0] as string;
            string textBefore = TextBox.Text.Substring(0, CurrentCorrectionPair.Start);
            string textAfter = TextBox.Text.Substring(CurrentCorrectionPair.Start + CurrentCorrectionPair.Length);
            TextBox.Text = textBefore + correctWord + textAfter;

            if (UnderlineMarks.Contains(CurrentCorrectionPair))
                UnderlineMarks.Remove(CurrentCorrectionPair);

            SuggestionsMenu.IsOpen = false;

            // ensure focus at the textbox again
            TextBox.Focus();

            AdjustCaretIndex();
            
        }

        private void TextBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) {

            int idx = TextBox.GetCharacterIndexFromPoint(e.GetPosition(this.TextBox), false);
            if (idx == -1)
                return;

            PositionedWord pWord = PositionedWord.FindPositionedWord(idx, TextBox.Text);
            if ((!UnderlineMarks.Any(m => m.AtSamePosition(pWord))))
                return;

            CurrentCorrectionPair = pWord;

            if(SuggestionsMenu==null)
                CreateSuggestionMenu();
            if (SuggestionsMenu.IsOpen)
                SuggestionsMenu.IsOpen = false;

            AddSuggestionItems(pWord.Word);
            SuggestionsMenu.IsOpen = true;


            e.Handled = true;
        }

        private void AddSuggestionItems(string word) {

            List<string> suggestions = new List<string>();
            using (Hunspell h = SpellCheck.GetHunspell(SpellCheck.GetLanguage(TextBox))) {
                if(h!=null)
                    suggestions = h.Suggest(word);
            }

            suggestionListBox.ItemsSource = suggestions;
            suggestionListBox.SelectedIndex = -1;
        }

        #endregion

        #region --- Render


        

        protected override void OnRender(System.Windows.Media.DrawingContext drawingContext) {

            if (String.IsNullOrEmpty(TextBox.Text))
                return;

            EnsureScrolling();

            FormattedText formattedText = new FormattedText(
                TextBox.Text,
                CultureInfo.InvariantCulture,
                TextBox.FlowDirection,
                new Typeface(TextBox.FontFamily.Source),
                TextBox.FontSize,
                TextBox.Foreground);

            

            if (TextBox.ViewportWidth > 0) {
                formattedText.MaxTextWidth = TextBox.ViewportWidth; // space for scrollbar
                //formattedText.MaxTextWidth = TextBox.ExtentWidth;                
            }

            double maxheight = Math.Max(TextBox.ActualHeight + TextBox.VerticalOffset, 0); //Adjust for scrolling
            if (maxheight > 0)
                formattedText.MaxTextHeight = maxheight;

            drawingContext.PushClip(new RectangleGeometry(new Rect(0, 0, TextBox.ActualWidth, TextBox.ActualHeight)));//restrict text to textbox

            DrawUnderlines(formattedText, drawingContext);

            //System.Console.Out.WriteLine(System.DateTime.Now + " repaint");

        }

        private void DrawUnderlines(FormattedText formattedText, System.Windows.Media.DrawingContext drawingContext) {



            double leftMargin = TextBox.BorderThickness.Left + TextBox.Padding.Left + SpellCheck.GetXOffset(TextBox);
            double topMargin = TextBox.BorderThickness.Top + TextBox.Padding.Top + SpellCheck.GetYOffset(TextBox) + 2;

            //ScrollViewer sv = GetScrollViewer();
            //if (sv != null) {
            //    topMargin = topMargin;
            //}
            

            foreach (PositionedWord p in UnderlineMarks) {

                int len = p.Start + p.Length > TextBox.Text.Length ? TextBox.Text.Length - p.Start : p.Length;
                if (len > 0) {

                    Geometry geom = formattedText.BuildHighlightGeometry(new Point(leftMargin, topMargin - TextBox.VerticalOffset), p.Start, len);

                    if (geom != null) {
                        StackedRectangleGeometryHelper srgh = new StackedRectangleGeometryHelper(geom);
                        foreach (Geometry g in srgh.BottomEdgeRectangleGeometries()) {
                            drawingContext.DrawGeometry(Brushes.Red, null, g);
                        }
                    }
                }

            }
        }

        private ScrollViewer GetScrollViewer() {            
            //if (VisualTreeHelper.GetChildrenCount(this.TextBox) == 0)
            //    return null;
            
            //DependencyObject dp = VisualTreeHelper.GetChild(this.TextBox, 0);
            //return VisualTreeHelper.GetChild(dp, 0) as ScrollViewer;
            return GetChild<ScrollViewer>(this.TextBox);
        }


        private void EnsureScrolling() {
            if (!mScrollingEventEnabled) {
                ScrollViewer sv = GetScrollViewer();
                if (sv == null) {
                    //System.Console.Out.WriteLine("não achou scroll");
                    return;
                }
                
                sv.ScrollChanged += new ScrollChangedEventHandler(ScrollChanged);                
                mScrollingEventEnabled = true;
            }
        }

        private void ScrollChanged(object sender, ScrollChangedEventArgs e) {
            //System.Console.Out.WriteLine(System.DateTime.Now + " rolou..  " + e.HorizontalChange + " x " + e.VerticalChange);
            if(e.HorizontalChange!=0 || e.VerticalChange!=0)
                this.InvalidateVisual();
            //TextBox.InvalidateVisual();
            
        }

        private T GetChild<T>(DependencyObject obj) {
            int t = VisualTreeHelper.GetChildrenCount(obj);
            if (t == 0)
                return default(T);
            for (int i = 0; i < t; i++) {
                DependencyObject myChild = VisualTreeHelper.GetChild(obj, i);
                if (myChild.GetType() == typeof(T)) {
                    return (T)(object)myChild;
                }
                var c = GetChild<T>(myChild);
                if (c != null)
                    return c;
            }
            return default(T);
        }

        #endregion

        

    }
}

